Description
Lightweight inline spellcheck: red wavy (undercurl) underline under misspelled words, with one-key suggest-and-replace. Catch obvious typos only โ not an intense/morphological checker. Must stay zero-cgo so the Homebrew/go-install build keeps working, and must not bloat the binary much.
RENDERING (undercurl)
- Misspelled-word spans emit raw SGR: undercurl '\e[4:3m' + underline color '\e[58:2::R:G:Bm' (theme-driven red, e.g. Flexoki red 217,54,42), reset '\e[59;4:0m'.
- lipgloss/termenv DON'T expose undercurl or underline-color โ add a raw-escape span attribute in the editor scanner. Preserve the markup-visible invariant: span text still concatenates to the raw line; only the SGR wrapper changes.
- Degrade gracefully: terminals without 4:3 show straight underline or ignore (Ghostty/kitty/WezTerm/foot/VTE support it). tmux needs terminal-features passthrough โ document it.
DICTIONARY (lightweight, pure-Go)
- Embed a single common-English wordlist (SCOWL-derived, ~50-60k most-common words; gzip-embedded via embed.FS, decompressed into a hashset at startup). Target small added binary weight, not full coverage.
- No cgo, no hunspell โ keeps 'go build .' / brew formula clean.
- Case-insensitive membership; treat possessives/simple plurals leniently if cheap.
SUGGEST & REPLACE
- Build a BK-tree from the same embedded dict at startup (edit-distance <=2 lookup; modest RAM, no extra binary weight; only queried on user trigger, never per render).
- Trigger key (suggest Ctrl+; TBD) when the cursor is on/adjacent to a flagged word opens a small popup of the top ~5 suggestions ranked by edit distance (tie-break by word frequency/length).
- Pick with arrows+Enter or a number key -> replace the misspelled word span in the buffer, mark dirty, clear its underline. Esc dismisses.
- The same popup includes an 'Add to dictionary' entry (writes the word to dict.txt) so add-word and replace share one UI.
WHAT TO SKIP (no false positives)
- Code fences and inline code, URLs, wikilinks [[...]], markdown link targets, YAML frontmatter values, and (when on in a code file) anything non-prose.
PERSONAL DICTIONARY (easy add)
- Plain file ~/.config/glint/dict.txt, one word per line, user-editable by hand.
- 'Add to dictionary' from the suggestion popup (or a direct add-word key) appends to dict.txt and clears the underline live.
TOGGLE / DEFAULTS
- On/off toggle (key TBD) for the session.
- Default ON for .md/.markdown/.txt and unnamed buffers; default OFF for recognized code-file extensions (driven by the same extension map as TASK-018 syntax highlighting โ share it).
- Config key to override the default (e.g. spellcheck = auto|on|off).
PERF
- Check only visible-viewport words per render; cache word->ok results so typing doesn't re-check the whole doc each keystroke; invalidate a word's cache entry when added to the personal dict. Suggestion lookups run only on trigger.
Acceptance Criteria
- #1 Misspelled prose words show a red undercurl underline in Ghostty; unsupported terminals degrade to plain/no underline
- #2 Dictionary is pure-Go embedded (no cgo); binary size increase is modest and 'go build .' + brew formula still work
- #3 Code fences, inline code, URLs, wikilinks, link targets, and frontmatter values are never flagged
- #4 Adding the word under the cursor appends to ~/.config/glint/dict.txt and removes the underline live; hand-editing dict.txt also works
- #5 Spellcheck defaults ON for md/txt/unnamed buffers and OFF for code files, with a config override and a session toggle
- #6 Triggering on a flagged word shows ~5 ranked suggestions; picking one replaces the word in place, marks the buffer dirty, and clears the underline